﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Mono.Cecil;
using System.Xml.Linq;
using DDUtils = CILADA.Engine.DynamicDependencyScanners.AnalysisUtils;
using Mono.Cecil.Cil;
using CILADA.Engine.DynamicDependencyScanners;
using System.Reflection;
using System.Xml;
using QuickGraph;
using QuickGraph.Algorithms.ConnectedComponents;
using System.Diagnostics;

namespace CILADA.Engine
{
    public class RuntimeCircularDependencyAnalyser : AnalyserBase
    {
        #region Fields
        private XmlDocument _methodParamsData = null;
        private List<string> _typesWithMethodsOfInterest = null;
        private IDictionary<string, List<ParameterAtPosition>> _paramsFromSignature = null;
        private string _currentAssemblyName = null;
        #endregion

        public RuntimeCircularDependencyAnalyser()
        {
            this.OutputDirectory = @"C:\Documents and Settings\andrew.esler\My Documents\Projects\CILADA\results"; //TODO: Remove this hard coding.
        }

        public override string AnalyseAssemblies(ICollection<string> assemblyPaths)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();

            HashSet<AssemblyReference> assemblyReferences = new HashSet<AssemblyReference>();
            foreach (string assembly in assemblyPaths)
            {
                List<AssemblyReference> assemResult = AnalyseAssembly(assembly, false);
                if (assemResult != null)
                {
                    assemblyReferences.UnionWith(assemResult);
                }
            }

            // filter out all the runtime deps, and save them to a GraphML file
            List<AssemblyReference> runtimeDeps = new List<AssemblyReference>();
            foreach (AssemblyReference ar in assemblyReferences)
            {
                if (ar.IsRuntimeDependency)
                {
                    runtimeDeps.Add(ar);
                }
            }
            // save them
            //GraphMLFormat.WriteAssemblyReferenceDependencies(runtimeDeps, this.OutputDirectory, this.OutputFileName);

            // TODO: fix the output file name...
            ICollection<List<string>> cycles = AnalysisUtils.CalculateCicularDependencies(assemblyReferences.ToList<AssemblyReference>());
            //GraphMLFormat.WriteCircularDependencies(cycles, this.OutputDirectory, this.OutputFileName + "_RTC");

            TimeSpan analysisTimeSpan = sw.Elapsed;
            sw.Stop();

            return this.GetAnalysisResults(assemblyPaths.Count, "Runtime", analysisTimeSpan);
        }

        public override void AnalyseAssembly(string assemblyPath)
        {
            AnalyseAssembly(assemblyPath, true);
        }

        private List<AssemblyReference> AnalyseAssembly(string assemblyPath, bool singleAnalysis)
        {
            if (_methodParamsData == null || _typesWithMethodsOfInterest == null)
            {
                LoadTypesFromXml();
            }

            AssemblyAnalysisResult result = AssemblyAnalysisResult.Unknown;
            FileInfo fi = new FileInfo(assemblyPath);

            AssemblyDefinition assemblyDef = null;
            try
            {
                assemblyDef = _assemReader.LoadAssembly(assemblyPath);
            }
            catch (BadImageFormatException)
            {
                result = AssemblyAnalysisResult.NotDotNetAssembly;
                NotDotNetAssemblyCount++;
            }
            catch (FileNotFoundException)
            {
                result = AssemblyAnalysisResult.AssemblyNotFound;
                AssemblyNotFoundCount++;
            }

            List<AssemblyReference> assemblyReferences = null;
            if (null != assemblyDef)
            {
                try
                {
                    // run analysis here
                    assemblyReferences = GetAssemblyReferencesFromDefinition(assemblyPath);
                    List<AssemblyReference> dynamicDeps = GetDynamicDependenciesFromAssembly(assemblyPath);

                    assemblyReferences.AddRange(dynamicDeps);
                    assemblyReferences.Sort();

                    if (singleAnalysis)
                    {
                        // write output here
                        // clear any vars that need it here
                    }

                    result = AssemblyAnalysisResult.AnalysisSuccessful;
                }
                catch (Exception e)
                {
                    result = AssemblyAnalysisResult.ErrorDuringProcessing;
                    ErrorDuringProcessingCount++;
                    Console.WriteLine("Error during processing! " + e.Message);
                }
            }

            // fire AssemblyAnalysed event
            AssemblyAnalysedEventArgs aae = new AssemblyAnalysedEventArgs(result, fi.FullName, null);
            if (AssemblyAnalysisResult.AnalysisSuccessful == result && null != assemblyDef)
            {
                aae.AssemblyProperties.Add("Name", GetStringOrNull(assemblyDef.Name));
                aae.AssemblyProperties.Add("Kind", GetStringOrNull(assemblyDef.Kind));
                aae.AssemblyProperties.Add("EntryPoint", GetStringOrNull(assemblyDef.EntryPoint));
            }
            OnAssemblyAnalysed(aae);

            return assemblyReferences;
        }

        private void LoadTypesFromXml()
        {
            Stream s = null;
            XmlDocument doc = new XmlDocument();
            try
            {
                s = Assembly.GetExecutingAssembly().GetManifestResourceStream("CILADA.Engine.Resources.DynamicDependencyMethodParameters.xml");
                doc.Load(s);
            }
            catch (Exception e)
            {
                Console.WriteLine("Error loading method signatures file: " + e.Message);

            }
            finally
            {
                if (s != null)
                {
                    s.Close();
                }
            }
            _methodParamsData = doc;

            XmlNodeList types = doc.GetElementsByTagName("type");
            _typesWithMethodsOfInterest = new List<string>();
            _paramsFromSignature = new Dictionary<string, List<ParameterAtPosition>>();
            try
            {
                foreach (XmlNode node in types)
                {
                    _typesWithMethodsOfInterest.Add(node.Attributes["name"].Value);
                    foreach (XmlNode method in node.SelectNodes("method"))
                    {
                        List<ParameterAtPosition> paramListForSig = new List<ParameterAtPosition>();
                        foreach (XmlNode parameter in method.SelectNodes("param"))
                        {
                            int number = Convert.ToInt32(parameter.Attributes["number"].Value);
                            string takesValue = parameter.Attributes["takes"].Value;
                            MethodParamTakes takes = (MethodParamTakes)Enum.Parse(typeof(MethodParamTakes), takesValue);

                            paramListForSig.Add(new ParameterAtPosition(number, takes));
                        }
                        _paramsFromSignature.Add(method.Attributes["signature"].Value, paramListForSig);
                    }
                }
            }
            catch (Exception e)
            {
                // TODO: Exception loading method signatures.
            }
        }

        private List<AssemblyReference> GetAssemblyReferencesFromDefinition(string assemblyPath)
        {
            HashSet<AssemblyReference> assemRefs = new HashSet<AssemblyReference>();
            _currentAssemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
            AssemblyDefinition assemDef = null;
            try
            {
                assemDef = _assemReader.LoadAssembly(assemblyPath);
                foreach (ModuleDefinition md in assemDef.Modules)
                {
                    foreach (AssemblyNameReference anr in md.AssemblyReferences)
                    {
                        assemRefs.Add(new AssemblyReference(assemDef.Name.Name, anr.Name));
                    }
                }
            }
            catch (Exception)
            {
                // do nothing
            }
            _currentAssemblyName = null;
            return assemRefs.ToList<AssemblyReference>();
        }

        private List<AssemblyReference> GetDynamicDependenciesFromAssembly(string assemblyPath)
        {
            List<AssemblyReference> dynamicDeps = new List<AssemblyReference>();
            _currentAssemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
            AssemblyDefinition assemDef = null;

            assemDef = _assemReader.LoadAssembly(assemblyPath);
            foreach (ModuleDefinition md in assemDef.Modules)
            {
                foreach (TypeDefinition t in md.Types)
                {
                    ProcessType(t, ref dynamicDeps);
                }
            }

            _currentAssemblyName = null;
            return dynamicDeps;
        }

        private void ProcessType(TypeDefinition type, ref List<AssemblyReference> dynamicDependencies)
        {
            // go through each method looking for given method calls
            foreach (PropertyDefinition pd in type.Properties)
            {
                if (pd.GetMethod != null) ProcessMethod(pd.GetMethod, ref dynamicDependencies);
                if (pd.SetMethod != null) ProcessMethod(pd.SetMethod, ref dynamicDependencies);
            }

            foreach (MethodDefinition cd in type.Constructors)
            {
                ProcessMethod(cd, ref dynamicDependencies);
            }

            foreach (MethodDefinition md in type.Methods)
            {
                ProcessMethod(md, ref dynamicDependencies);
            }

            foreach (TypeDefinition t in type.NestedTypes)
            {
                ProcessType(t, ref dynamicDependencies);
            }
        }

        private void ProcessMethod(MethodDefinition method, ref  List<AssemblyReference> dynamicDependencies)
        {
            if (method == null)
            {
                return;
            }

            if (DDUtils.UsesParamsKeyword(method))
            {
                // cant process the method as there is no known number of parameters.
                return;
            }

            if (method.HasBody)
            {
                SequencePoint mostRecentSeqPoint = null;
                Instruction instruction = null;
                for (int instrIndex = 0; instrIndex < method.Body.Instructions.Count; instrIndex++)
                {
                    instruction = method.Body.Instructions[instrIndex];
                    if (instruction.SequencePoint != null)
                    {
                        mostRecentSeqPoint = instruction.SequencePoint;
                    }
                    if (OpCodes.Call.Equals(instruction.OpCode) || OpCodes.Callvirt.Equals(instruction.OpCode) || OpCodes.Calli.Equals(instruction.OpCode))
                    {
                        var methodReference = instruction.Operand as MethodReference;
                        if (methodReference != null)
                        {
                            if (_typesWithMethodsOfInterest.Contains(methodReference.DeclaringType.FullName))
                            {
                                AssemblyReference ar = ProcessInstruction(method, methodReference, instruction, instrIndex);
                                if (ar != null)
                                {
                                    dynamicDependencies.Add(ar);
                                }
                            }
                        }
                    }
                }
            }
        }

        private AssemblyReference ProcessInstruction(MethodDefinition method, MethodReference usedMethod, Instruction instruction, int instrIndex)
        {
            //Console.WriteLine("MethodSig=" + usedMethod.ToString());

            List<ParameterAtPosition> paramsToGet = null;
            string typeMethod = usedMethod.DeclaringType.FullName + "::" + usedMethod.Name;
            if (_paramsFromSignature.Keys.Contains(usedMethod.ToString()))
            {
                paramsToGet = _paramsFromSignature[usedMethod.ToString()];
            }
            else
            {
                // signature hasnt been added, dont know how to process it.
                return null;
            }

            if (paramsToGet != null)
            {
                //Console.WriteLine("Processing {0}'s instruction {1}", typeMethod, usedMethod.ToString());
                AssemblyReference ar = new AssemblyReference();
                ar.From = _currentAssemblyName;
                ar.To = "unknown";
                ar.Method = method.ToString();
                ar.IsRuntimeDependency = true;

                foreach (ParameterAtPosition pap in paramsToGet)
                {
                    string paramValue = null;
                    paramValue = DDUtils.FindValueOfIthStringParameter(method, instrIndex, usedMethod, pap.Position);

                    if (pap.ParamTakes.Equals(MethodParamTakes.assembly))
                    {
                        ar.To = paramValue;
                    }
                    else if (pap.ParamTakes.Equals(MethodParamTakes.type))
                    {
                        ar.Type = paramValue;
                    }

                    //Console.WriteLine("\nGetting parameter {0} of type {1} from method {2}, called from {3}. Value={4}", pap.Position, pap.ParamTakes, usedMethod.ToString(), method.ToString(), paramValue);
                }
                return ar;
            }
            else
            {
                // couldnt match method.
                return null;
            }
        }
    }
}
